﻿// Copyright (c) 2010 Dan Popick

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Media;
using System.Windows.Media.Imaging;

using Open.Web.Stijl.Css;
using Open.Web.Stijl.Utility;

namespace Open.Web.Stijl
{
    public class TiledImage : CachedResourceInfo
    {
		static Tracer<TiledImage> _log = new Tracer<TiledImage>();
	
		public string FileName { get; protected set; }
        public List<Tile> Tiles { get; set; }
        public byte[] ImageData { get; set; }
        public string MimeType { get; set; }
        public ColorSet Colors { get; set; }

        public TiledImage()
        {
            Tiles = new List<Tile>();
            Colors = new ColorSet();
        }

        public TiledImage(IEnumerable<Tile> tiles, ColorSet colorSet)
        {
            Tiles = new List<Tile>(tiles);
            Colors = colorSet;
        }

        void ComputeHeaderInfo()
        {
            Crc32 crc = new Crc32();
            DateTime maxMod = DateTime.MinValue;
            crc.ComputeHash(ImageData);

            Tiles.ForEach(x =>
                {
                    FileInfo fi = new FileInfo(x.Sprite.LocalPath);
                    maxMod = maxMod < fi.LastWriteTimeUtc ? fi.LastWriteTimeUtc : maxMod;
                });

            LastModified = maxMod.ToString("r");
            ETag = String.Format("\"{0:x}\"", crc.CrcValue);
        }

		public void SetFilename(string filename)
		{
			FileName = filename;

			UpdateRuleSets();
		}

        /// <summary>
        /// Generates a single composite image. Must be called before accessing the ImageData
        /// </summary>
        /// <param name="fileName">The base filename to use when altering the CSS rulesets.</param>
        public void GenerateImage()  
        {
            if (Tiles == null || Tiles.Count == 0)
                throw new InvalidOperationException("Image must contain at least one tile.");

			Size imSize = GetImageDimensions();

            // set the output format, useful for the differentiation between alpha/non-alpha full color PNGs
            System.Drawing.Imaging.PixelFormat workingFormat;
            if (Tiles[0].Sprite.Format.IsIndexed())
                // indexed color defaults to 24-bit
                workingFormat = System.Drawing.Imaging.PixelFormat.Format24bppRgb;
            else
                workingFormat = Tiles[0].Sprite.Format;

            Bitmap bm = new Bitmap(imSize.Width, imSize.Height, workingFormat);
            Graphics g = Graphics.FromImage(bm);
			g.PageUnit = GraphicsUnit.Pixel;
			_log.Debug("{0}: Creating new graphics context {1}x{2}", FileName, imSize.Width, imSize.Height);

            // for indexed images, paint the whole canvas with some known color to avoid any
            // unset pixels
            if (Tiles[0].Sprite.Format.IsIndexed())
            {
                SolidBrush backgroundBrush = new SolidBrush(Colors.First());
				g.FillRectangle(backgroundBrush, 0, 0, imSize.Width, imSize.Height);
            }

            // place tile in the new image
			Tiles.ForEach(x => DrawTiledImage(g, x));

            Bitmap outputImage;
            if (Tiles[0].Sprite.Format.IsIndexed())
                // convert to an indexed image
                ImageData = ConvertTo8bpp(bm);
            else
            {
                // stream the non-indexed image out as-is
                outputImage = bm;
                MemoryStream ms = new MemoryStream();
                outputImage.Save(ms, ImageFormat.Png);

                ImageData = ms.ToArray();
            }

            ComputeHeaderInfo();

            return;
        }

		/// <summary>
		/// Determines the image dimensions
		/// </summary>
		/// <returns>A size representing the extent of the final image</returns>
		protected virtual Size GetImageDimensions()
		{
			return new Size(
				Tiles.Max(x => x.Location.X + x.Size.Width),
				Tiles.Max(x => x.Location.Y + x.Size.Height));
		}

		/// <summary>
		/// Draws the image tile into the graphics context
		/// </summary>
		/// <param name="graphics">The graphics context to draw into</param>
		/// <param name="tile">The tiled image to draw</param>
		protected virtual void DrawTiledImage(Graphics graphics, Tile tile)
		{
			Image im = tile.Sprite.GetImage();
			graphics.DrawImage(im, new Rectangle(tile.Location.X, tile.Location.Y, im.Width, im.Height));
		}

        /// <summary>
        /// Converts an image to an 8bpp bitmap
        /// </summary>
        /// <param name="sourceBitmap"></param>
        /// <returns></returns>
        /// <remarks>
        /// This was a source of great consternation
        /// http://stackoverflow.com/questions/4356973/easiest-way-to-combine-several-png8-images-in-net
        /// </remarks>
        byte[] ConvertTo8bpp(Bitmap sourceBitmap)
        {
            // generate a custom palette for the bitmap
            Dictionary<System.Drawing.Color, byte> colorDict = new Dictionary<System.Drawing.Color, byte>();
            List<System.Windows.Media.Color> colorList = new List<System.Windows.Media.Color>();
            byte index = 0;
            unchecked
            {
                foreach (var cc in Colors)
                {
                    colorDict[cc] = index++;
                    colorList.Add(cc.ToMediaColor());
                }
            }
            System.Windows.Media.Imaging.BitmapPalette bmpPal = new System.Windows.Media.Imaging.BitmapPalette(colorList);

            // build the image data for the new image
            int width = sourceBitmap.Width;
            int height = sourceBitmap.Height;
            int stride = sourceBitmap.Width;
            byte[] imageData = new byte[width * height];

            for (int x = 0; x < width; ++x)
                for (int y = 0; y < height; ++y)
                {
                    var pixelColor = sourceBitmap.GetPixel(x, y);
                    imageData[x + (stride * y)] = colorDict[pixelColor];
                }

            // generate the image
            var bsource = BitmapSource.Create(width, height, 96, 96, PixelFormats.Indexed8, bmpPal, imageData, stride);

            // encode the image
            PngBitmapEncoder encoder = new PngBitmapEncoder();
            MemoryStream outputStream = new MemoryStream();
            encoder.Interlace = PngInterlaceOption.Off;
            encoder.Frames.Add(BitmapFrame.Create(bsource));
            encoder.Save(outputStream);
            outputStream.Close();

            return outputStream.ToArray();
        }

        public void UpdateRuleSets()
        {
            Tiles.ForEach(x => x.Sprite.RuleSets.ForEach(rs =>
                {
                    rs.SpriteXOffset = -x.Location.X;
                    rs.SpriteYOffset = -x.Location.Y;
                    rs.Url = FileName;
                }));
        }
    }
}
